package com.atguigu.serurity.filter;

import com.atguigu.commonutils.R;
import com.atguigu.commonutils.ResponseUtil;

import com.atguigu.serurity.entity.SecurityUser;
import com.atguigu.serurity.entity.User;
import com.atguigu.serurity.security.TokenManager;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/*
	登录认证过滤器，需要重写三个方法：
		attemptAuthentication：初步认证方法，用于从请求体中，获取到用户提交的请求参数，并调用 “认证管理器” 的方法，进行认证
    	successfulAuthentication： 认证成功回调函数，当用户认证成功之后，会调用这个函数，可以用于认证成功之后，返回加密 token 信息
    	unsuccessfulAuthentication： 认证失败回调函数，当认证失败之后，会调用这个函数，可以用于返回认证错误的信息
 */
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    // 通过 通过 authenticationManager() 方法 获取到的 认证管理器
    private final AuthenticationManager authenticationManager;
    // 自定义的 token 管理器，用于 加密/解密 token
    private final TokenManager tokenManager;
    // redis 缓存对象
    private final RedisTemplate<String, List<String>> redisTemplate;

    // bean 注入
    public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager,
                            RedisTemplate<String, List<String>> redisTemplate) {

        this.authenticationManager = authenticationManager;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        // 关闭身份认证只能为 post 设置
        this.setPostOnly(false);
        // 设置认证HTTP路径 和 请求方式(不区分大小写)
        this.setRequiresAuthenticationRequestMatcher(
                new AntPathRequestMatcher("/admin/acl/login","POST"));
    }

    /**
     * 初步进行验证的方法，只进行了简单的参数处理
     * @param req 请求对象
     * @param res 响应对象
     * @return 验证封装类，里面记录了认证成功后的相关信息
     * @throws AuthenticationException 如果认证失败，就会抛出这个异常
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
        try {

            // ObjectMapper: 是 jackson 的 JSON 转换工具，可以将指定 “JSON字符串” 转换为指定的 class 类型
            // req.getInputStream(): 以 Stream 流的形式，读取到请求正文（请求体）
            User user = new ObjectMapper().readValue(req.getInputStream(), User.class);

            // 调用 “认证管理器” 进行身份认证
            return authenticationManager.authenticate(
                    // 创建登录认证对象 UsernamePasswordAuthenticationToken 用于进行验证获取参数（就是一个包装类）
                    // 需要设置参数：用户名、用户密码、权限数组（可选）
                    // 参数 权限数组 是一个可选值：
                    // 		我们这里直接创建一个集合即可，不需要传参，他只是用于设置 setAuthenticated(true)
                    // setAuthenticated(true)：他可以设置isAuthenticated() 方法返回 true
                    // isAuthenticated()：如果在成功身份验证后，返回的是一个 “不可变” 的身份验证令牌，
                    // 					  在这种情况下，使isAuthenticated() 返回 true 可以提高效率
                    // 					  参考源码：UsernamePasswordAuthenticationToken 类 54 行注释
                    new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>())
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * springSecurity 认证成功回调函数
     * @param req 请求对象
     * @param res 响应对象
     * @param chain  过滤链，也可以称之为调用链，<br>
     *               该对象只有一个 doFilter 方法，用来调用下游过滤器，<br>
     *               我们这里认证成功了，直接返回即可，无需继续调用
     *               <p>参考文章：https://blog.csdn.net/Jianyang_usst/article/details/121381839</p>
     * @param auth springSecurity 验证对象，里面保存了登录用户的相关信息
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,
                                            FilterChain chain, Authentication auth) throws IOException, ServletException {
        // Authentication：认证成功之后，springSecurity 中保存的用户信息
        // auth.getPrincipal()：返回认证成功的用户主体
        // 这个主体就是我们前面实现了 UserDetails 的那个实现类，认证成功之后交由 springSecurity 进行管理的那个
        SecurityUser user = (SecurityUser) auth.getPrincipal();
        // 根据用户名，进行加密 token
        String token = tokenManager.createToken(user.getUsername());
        // 根据用户名，保存用户权限信息
        redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
        // 返回 token 到前端
        ResponseUtil.out(res, R.ok().data("token", token));
    }

    /**
     * 登录失败
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(response, R.error().message("用户名或密码错误"));
    }
}